Import-Module -Name 'Defender'

function Test-WDEGAvailable
{
    [CmdletBinding()]
    [OutputType([Boolean])]
    param ()

    # Check OS Version - Minimum version allowed is Windows 10, update 1709
    $minimumOSVersionRequired = [Version]'10.0.16299'

    # The version of PS6 we use for policy has a bug retrieving the OS version which requires us to use full PS
    $osVersion = powershell -OutputFormat 'Xml' -NonInteractive -Command { [Environment]::OSVersion.Version }
    Write-Verbose -Message "Found machine OS version: $osVersion" -Verbose

    $wdegAvailable = ($osVersion -ge $minimumOSVersionRequired)
    return $wdegAvailable
}

function Get-ExpectedAttackSurfaceReductionRuleIdsToNamesTable
{
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param ()

    # Explanation of GUIDS can be found here: https://docs.microsoft.com/en-us/windows/security/threat-protection/windows-defender-exploit-guard/attack-surface-reduction-exploit-guard
    # Do not expect rules that require cloud-delivered protection
    $attackRuleIdToName = @{
        'be9ba2d9-53ea-4cdc-84e5-9b1eeee46550' = 'Block executable content from email client and webmail'
        'b2b3f03d-6a65-4f7b-a9c7-1c7ef74a9ba4' = 'Block untrusted and unsigned processes that run from USB'
        '9e6c4e1f-7d60-472f-ba1a-a39ef669e4b2' = 'Block credential stealing from the Windows local security authority subsystem (lsass.exe)'
        'd4f940ab-401b-4efc-aadc-ad5f3c50688a' = 'Block all Office applications from creating child processes'
        'd3e037e1-3eb8-44c8-a917-57927947596d' = 'Block JavaScript or VBScript from launching downloaded executable content'
        '5beb7efe-fd9a-4556-801d-275e5ffc04cc' = 'Block execution of potentially obfuscated scripts'
        '3b576869-a4ec-4529-8536-b80a7769e899' = 'Block Office applications from creating executable content'
        '26190899-1602-49e8-8b27-eb1d0a1ce869' = 'Block Office communication application from creating child processes'
        '92E97FA1-2EDF-4476-BDD6-9DD0B4DDDC7B' = 'Block Win32 API calls from Office macro'
        '7674ba52-37eb-4a4f-a9a1-f0f9a1619a2c' = 'Block Adobe Reader from creating child processes'
    }

    return $attackRuleIdToName
}

function Get-ExpectedAttackSurfaceReductionRuleIdList
{
    [CmdletBinding()]
    [OutputType([String[]])]
    param ()

    $attackSurfaceReductionRuleIds = (Get-ExpectedAttackSurfaceReductionRuleIdsToNamesTable).Keys
    return $attackSurfaceReductionRuleIds
}

function Get-ExpectedAttackSurfaceReductionRuleName
{
    [CmdletBinding()]
    [OutputType([String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [String]
        $Id
    )

    $attackRuleIdToName = Get-ExpectedAttackSurfaceReductionRuleIdsToNamesTable
    return $attackRuleIdToName[$Id]
}

function Get-NotEnabledAttackSurfaceReductionRuleIdList
{
    [CmdletBinding()]
    [OutputType([String[]])]
    param
    (
        [Parameter(Mandatory = $true)]
        [CimInstance]
        $MPPreference
    )

    $notEnabledIds = @()

    $actualAttackSurfaceReductionIds = @($MPPreference.AttackSurfaceReductionRules_Ids)
    $actualAttackSurfaceReductionActions = @($MPPreference.AttackSurfaceReductionRules_Actions)

    $expectedIds = Get-ExpectedAttackSurfaceReductionRuleIdList

    foreach ($expectedId in $expectedIds)
    {
        $idIndex = [Array]::IndexOf($actualAttackSurfaceReductionIds, $expectedId)

        if ($idIndex -ge 0)
        {
            if ($idIndex -lt $actualAttackSurfaceReductionActions.Count)
            {
                $idAction = $actualAttackSurfaceReductionActions[$idIndex]

                if ($idAction -eq 1)
                {
                    Write-Verbose -Message "Attack surface reduction rule with ID '$expectedId' is enabled." -Verbose
                }
                else
                {
                    # ID action is not enabled
                    Write-Verbose -Message "Attack surface reduction rule with ID '$expectedId' is not enabled. Found action state '$idAction' when expected state is '1'." -Verbose
                    $notEnabledIds += $expectedId
                }
            }
            else
            {
                # ID has no action state
                Write-Verbose -Message "Attack surface reduction rule with ID '$expectedId' is not enabled. Found no action state when expected state is '1'." -Verbose
                $notEnabledIds += $expectedId
            }
        }
        else
        {
            # ID is not present
            Write-Verbose -Message "Attack surface reduction rule with ID '$expectedId' is not enabled. ID was not found in list of current IDs." -Verbose
            $notEnabledIds += $expectedId
        }
    }

    return $notEnabledIds
}

function Get-ReasonsExploitGuardNotEnabled
{
    [CmdletBinding()]
    [OutputType([Hashtable[]])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Compliant', 'Non-Compliant')]
        [String]
        $NotAvailableMachineState
    )

    $reasons = @()
    $reasonCodePrefix = 'WindowsDefender:WindowsDefenderExploitGuard:'

    if (-not (Test-WDEGAvailable))
    {
        if ($NotAvailableMachineState -eq 'Non-Compliant')
        {
            $reason = @{
                Code = $reasonCodePrefix + 'ExploitGuardNotAvailable'
                Phrase = "The Windows Defender Exploit Guard feature is not available on this machine. This feature is only available on machines that have at least Windows 10, update 1709 (10.0.16299)."
            }
            $reasons += $reason
        }

        return $reasons
    }

    # Retrieve information about the configuration of Windows Defender
    $mpPreference = Get-MpPreference

    if ($null -eq $mpPreference)
    {
        $reason = @{
            Code = $reasonCodePrefix + 'FailedToRetrieveMpPreference'
            Phrase = "Failed to retrieve the preferences for Windows Defender scans and updates on this machine."
        }
        $reasons += $reason
    }
    else
    {
        # Check that Controlled Folder Access is enabled
        if ($mpPreference.EnableControlledFolderAccess -eq 1)
        {
            Write-Verbose -Message 'Controlled Folder Access is enabled on this machine.' -Verbose
        }
        else
        {
            $reason = @{
                Code = $reasonCodePrefix + 'ControlledFolderAccessNotEnabled'
                Phrase = "Controlled Folder Access is not enabled on this machine. The current value is '$($mpPreference.EnableControlledFolderAccess)', but the expected value is '1'."
            }
            $reasons += $reason
        }

        # Check the configured actions for ASR rules
        $nonCompliantRuleIDs = @(Get-NotEnabledAttackSurfaceReductionRuleIdList -MPPreference $mpPreference)

        foreach ($nonCompliantRuleID in $nonCompliantRuleIDs)
        {
            $ruleName = Get-ExpectedAttackSurfaceReductionRuleName -Id $nonCompliantRuleID
            $reason = @{
                Code = $reasonCodePrefix + 'AttackSurfaceReductionRuleNotEnabled'
                Phrase = "The Attack Surface Reduction rule with the id '$nonCompliantRuleID' and the name '$ruleName' is not enabled on this machine."
            }
            $reasons += $reason
        }
    }

    return $reasons
}

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Yes')]
        [String]
        $IsSingleInstance,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Compliant', 'Non-Compliant')]
        [String]
        $NotAvailableMachineState
    )

    $exploitGuardInfo = @{
        IsSingleInstance = 'Yes'
        NotAvailableMachineState = $NotAvailableMachineState
    }

    $reasons = @(Get-ReasonsExploitGuardNotEnabled -NotAvailableMachineState $NotAvailableMachineState)

    if ($null -ne $reasons -and $reasons.Count -gt 0)
    {
        $exploitGuardInfo['Reasons'] = $reasons
    }

    return $exploitGuardInfo
}

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Yes')]
        [String]
        $IsSingleInstance,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Compliant', 'Non-Compliant')]
        [String]
        $NotAvailableMachineState
    )

    $reasons = @(Get-ReasonsExploitGuardNotEnabled -NotAvailableMachineState $NotAvailableMachineState)

    if ($null -ne $reasons -and $reasons.Count -gt 0)
    {
        return $false
    }

    return $true
}

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateSet('Yes')]
        [String]
        $IsSingleInstance,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Compliant', 'Non-Compliant')]
        [String]
        $NotAvailableMachineState
    )

    if (-not (Test-WDEGAvailable))
    {
        Write-Verbose -Message 'Skipping enabling Windows Defender Exploit Guard recommendations. Windows Defender Exploit Guard is not available on this machine. This feature is only available on machines that have at least Windows 10, update 1709 (10.0.16299).' -Verbose
        return
    }

    Write-Verbose -Message "Setting EnableControlledFolderAccess to 'Enabled'..." -Verbose
    $null = Set-MpPreference -EnableControlledFolderAccess 'Enabled'

    $expectedIds = Get-ExpectedAttackSurfaceReductionRuleIdList
    $enableActions = @('Enabled') * $expectedIds.Count
    
    Write-Verbose -Message "Setting expected AttackSurfaceReductionRules to 'Enabled'..." -Verbose
    $null = Add-MpPreference -AttackSurfaceReductionRules_Ids $expectedIds -AttackSurfaceReductionRules_Actions $enableActions

    Write-Verbose -Message 'Finished enabling Windows Defender Exploit Guard recommendations.' -Verbose
}
